@fiale-plus/pi-rogue-bundle 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -355,10 +355,11 @@ function recoverReviewControl(state: SessionState): void {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
type AdvisorHintDetails = {
|
|
358
|
+
kind?: "handoff" | "answer";
|
|
358
359
|
decision?: "continue" | "review" | "defer";
|
|
359
360
|
reason?: string;
|
|
360
361
|
summary?: string;
|
|
361
|
-
actions?:
|
|
362
|
+
actions?: unknown;
|
|
362
363
|
};
|
|
363
364
|
|
|
364
365
|
type ReviewControlState = {
|
|
@@ -379,37 +380,77 @@ type ReviewMaterialMeta = {
|
|
|
379
380
|
isAgentEnd: boolean;
|
|
380
381
|
materialSignals?: string[];
|
|
381
382
|
};
|
|
382
|
-
|
|
383
|
+
|
|
384
|
+
function normalizeAdvisorActions(actions: unknown): string[] {
|
|
385
|
+
const raw = Array.isArray(actions) ? actions : typeof actions === "string" ? [actions] : [];
|
|
386
|
+
return raw.map((action) => squish(action, 200)).filter(Boolean).slice(0, 2);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function advisorHandoffText(decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []): string {
|
|
390
|
+
const limitedActions = normalizeAdvisorActions(actions);
|
|
391
|
+
return [
|
|
392
|
+
`Advisor verdict: ${decision}.`,
|
|
393
|
+
reason ? `Reason: ${reason}` : "",
|
|
394
|
+
summary ? `Summary: ${summary}` : "",
|
|
395
|
+
limitedActions.length ? `Actions: ${limitedActions.join("; ")}` : "",
|
|
396
|
+
].filter(Boolean).join("\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function sendAdvisorHint(pi: ExtensionAPI, decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []) {
|
|
400
|
+
const limitedActions = normalizeAdvisorActions(actions);
|
|
383
401
|
pi.sendMessage(
|
|
384
402
|
{
|
|
385
403
|
customType: "advisor:llm",
|
|
386
|
-
content: reason,
|
|
404
|
+
content: advisorHandoffText(decision, reason, summary, limitedActions),
|
|
387
405
|
display: true,
|
|
388
|
-
details: { decision, reason, summary, actions:
|
|
406
|
+
details: { decision, reason, summary, actions: limitedActions },
|
|
389
407
|
},
|
|
390
408
|
{ deliverAs: "followUp" },
|
|
391
409
|
);
|
|
392
410
|
}
|
|
393
411
|
|
|
412
|
+
function sendAdvisorAnswer(pi: ExtensionAPI, text: string) {
|
|
413
|
+
pi.sendMessage({
|
|
414
|
+
customType: "advisor:llm",
|
|
415
|
+
content: text,
|
|
416
|
+
display: true,
|
|
417
|
+
details: { kind: "answer", summary: text },
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
394
421
|
function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme: any) {
|
|
395
422
|
const details = (message?.details ?? {}) as AdvisorHintDetails;
|
|
396
423
|
const customType = String(message?.customType ?? "advisor:rules");
|
|
397
|
-
const decision = details.decision ?? "defer";
|
|
398
424
|
const sourceColor = customType === "advisor:llm" ? "success" : customType === "advisor:model" ? "accent" : "muted";
|
|
399
|
-
const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
|
|
400
425
|
const source = theme.bold(theme.fg(sourceColor, `[${customType}]`));
|
|
426
|
+
|
|
427
|
+
if (details.kind === "answer") {
|
|
428
|
+
const body = contentText(message?.content) || details.summary || "No advisor response.";
|
|
429
|
+
const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
|
|
430
|
+
box.addChild(new Text(`${theme.bold(theme.fg("success", "↗"))} ${source} ${theme.bold(theme.fg("success", "answer"))}`, 0, 0));
|
|
431
|
+
box.addChild(new Text(theme.fg("dim", body), 0, 0));
|
|
432
|
+
return box;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const decision = details.decision ?? "defer";
|
|
436
|
+
const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
|
|
401
437
|
const verdict = theme.bold(theme.fg(decisionColor, decision));
|
|
402
438
|
const glyph = decision === "review" ? "↗" : decision === "defer" ? "…" : "·";
|
|
403
439
|
const reason = squish(details.reason || contentText(message?.content) || "no extra detail", 180);
|
|
440
|
+
const actions = normalizeAdvisorActions(details.actions);
|
|
404
441
|
|
|
405
442
|
const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
|
|
406
|
-
box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict}
|
|
443
|
+
box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict}`, 0, 0));
|
|
444
|
+
box.addChild(new Text(theme.fg("dim", `reason: ${reason}`), 0, 0));
|
|
407
445
|
|
|
408
|
-
if (
|
|
446
|
+
if (details.summary) {
|
|
409
447
|
box.addChild(new Text(theme.fg("dim", `summary: ${squish(details.summary, 220)}`), 0, 0));
|
|
410
448
|
}
|
|
411
|
-
if (
|
|
412
|
-
box.addChild(new Text(theme.fg("dim", `actions: ${
|
|
449
|
+
if (actions.length) {
|
|
450
|
+
box.addChild(new Text(theme.fg("dim", `actions: ${actions.map((a) => squish(a, 80)).join(" • ")}`), 0, 0));
|
|
451
|
+
}
|
|
452
|
+
if (!options.expanded && contentText(message?.content).split("\n").length > 3) {
|
|
453
|
+
box.addChild(new Text(theme.fg("dim", "Ctrl+O full advisor handoff"), 0, 0));
|
|
413
454
|
}
|
|
414
455
|
|
|
415
456
|
return box;
|
|
@@ -933,14 +974,15 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
933
974
|
: json.verdict === "not_done" ? "review"
|
|
934
975
|
: "defer";
|
|
935
976
|
finalDecision = decision;
|
|
936
|
-
|
|
977
|
+
const rawReason = json.reason || json.summary || "review result";
|
|
978
|
+
finalReason = rawReason.slice(0, 120);
|
|
937
979
|
|
|
938
980
|
const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
|
|
939
981
|
writeText(CURRENT_PATH, `${display}\n`);
|
|
940
|
-
sendAdvisorHint(pi, decision,
|
|
982
|
+
sendAdvisorHint(pi, decision, rawReason, json.summary || "", json.actions || []);
|
|
941
983
|
|
|
942
984
|
if (json.verdict !== "on_track") {
|
|
943
|
-
state.followUp = [json.summary, ...(json.actions
|
|
985
|
+
state.followUp = [json.summary, ...normalizeAdvisorActions(json.actions)].filter(Boolean).join(" — ");
|
|
944
986
|
}
|
|
945
987
|
|
|
946
988
|
markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
|
|
@@ -1326,7 +1368,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1326
1368
|
|
|
1327
1369
|
// Anything else: treat as a question to the advisor
|
|
1328
1370
|
const r = await askAdvisor(pi, ctx, a, "slash", true);
|
|
1329
|
-
|
|
1371
|
+
if (r.error) {
|
|
1372
|
+
ctx.ui.notify(r.text, "warning");
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
sendAdvisorAnswer(pi, r.text);
|
|
1330
1376
|
},
|
|
1331
1377
|
});
|
|
1332
1378
|
}
|
|
@@ -17,9 +17,11 @@ vi.mock("@earendil-works/pi-ai", async () => {
|
|
|
17
17
|
type Handler = (event: any, ctx: any) => any;
|
|
18
18
|
|
|
19
19
|
type HandlerMap = Record<string, Handler[]>;
|
|
20
|
+
type CommandMap = Record<string, { handler: (args: string, ctx: any) => any }>;
|
|
20
21
|
|
|
21
22
|
function makeHandlers() {
|
|
22
23
|
const handlers: HandlerMap = {};
|
|
24
|
+
const commands: CommandMap = {};
|
|
23
25
|
const sendMessage = vi.fn();
|
|
24
26
|
|
|
25
27
|
const pi = {
|
|
@@ -28,7 +30,9 @@ function makeHandlers() {
|
|
|
28
30
|
handlers[event].push(handler);
|
|
29
31
|
},
|
|
30
32
|
registerMessageRenderer: () => undefined,
|
|
31
|
-
registerCommand: () =>
|
|
33
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => {
|
|
34
|
+
commands[name] = command;
|
|
35
|
+
},
|
|
32
36
|
registerTool: vi.fn(),
|
|
33
37
|
sendMessage,
|
|
34
38
|
sendUserMessage: () => undefined,
|
|
@@ -38,12 +42,13 @@ function makeHandlers() {
|
|
|
38
42
|
},
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
return { handlers, pi: pi as any, sendMessage };
|
|
45
|
+
return { handlers, commands, pi: pi as any, sendMessage };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
45
49
|
const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
|
|
46
50
|
const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
|
|
51
|
+
const ADVISOR_CACHE_PATH = join(ADVISOR_STATE_DIR, "cache.json");
|
|
47
52
|
|
|
48
53
|
function readAdvisorState(): any {
|
|
49
54
|
return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
@@ -73,21 +78,26 @@ function mkCtx() {
|
|
|
73
78
|
describe("advisor two-agent convergence", () => {
|
|
74
79
|
let ctx: any;
|
|
75
80
|
let handlers: HandlerMap;
|
|
81
|
+
let commands: CommandMap;
|
|
76
82
|
let sendMessageMock: ReturnType<typeof vi.fn>;
|
|
77
83
|
let completeSimpleMock: ReturnType<typeof vi.fn>;
|
|
78
84
|
let priorState: string | null = null;
|
|
79
85
|
let priorConfig: string | null = null;
|
|
86
|
+
let priorCache: string | null = null;
|
|
80
87
|
|
|
81
88
|
beforeEach(() => {
|
|
82
89
|
priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
|
|
83
90
|
priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
|
|
91
|
+
priorCache = existsSync(ADVISOR_CACHE_PATH) ? readFileSync(ADVISOR_CACHE_PATH, "utf8") : null;
|
|
84
92
|
|
|
85
93
|
const setup = makeHandlers();
|
|
86
94
|
handlers = setup.handlers;
|
|
95
|
+
commands = setup.commands;
|
|
87
96
|
sendMessageMock = setup.sendMessage;
|
|
88
97
|
|
|
89
98
|
mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
|
|
90
99
|
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
|
|
100
|
+
writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
|
|
91
101
|
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
|
|
92
102
|
turns: 0,
|
|
93
103
|
lastTask: "",
|
|
@@ -136,6 +146,12 @@ describe("advisor two-agent convergence", () => {
|
|
|
136
146
|
} else {
|
|
137
147
|
writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
|
|
138
148
|
}
|
|
149
|
+
|
|
150
|
+
if (priorCache === null) {
|
|
151
|
+
writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
|
|
152
|
+
} else {
|
|
153
|
+
writeFileSync(ADVISOR_CACHE_PATH, priorCache, "utf8");
|
|
154
|
+
}
|
|
139
155
|
});
|
|
140
156
|
|
|
141
157
|
it("does not re-run advisory review on repeated material snapshots", async () => {
|
|
@@ -160,6 +176,17 @@ describe("advisor two-agent convergence", () => {
|
|
|
160
176
|
const firstState = readAdvisorState();
|
|
161
177
|
expect(firstState.reviewControl.lastDecision).toBe("review");
|
|
162
178
|
expect(firstState.followUp).toContain("Closeout is incomplete");
|
|
179
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
customType: "advisor:llm",
|
|
182
|
+
content: expect.stringContaining("Summary: Closeout is incomplete"),
|
|
183
|
+
}),
|
|
184
|
+
expect.anything(),
|
|
185
|
+
);
|
|
186
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
187
|
+
expect.objectContaining({ content: expect.stringContaining("Actions: run focused check") }),
|
|
188
|
+
expect.anything(),
|
|
189
|
+
);
|
|
163
190
|
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
164
191
|
|
|
165
192
|
const consumedPrompt = await preflight;
|
|
@@ -183,6 +210,71 @@ describe("advisor two-agent convergence", () => {
|
|
|
183
210
|
expect(String(withoutFollowUp?.systemPrompt)).not.toContain("Advisor follow-up");
|
|
184
211
|
});
|
|
185
212
|
|
|
213
|
+
it("normalizes string actions in advisor handoffs", async () => {
|
|
214
|
+
const preflight = handlers.before_agent_start;
|
|
215
|
+
const turnEnd = handlers.turn_end;
|
|
216
|
+
expect(preflight?.length).toBe(1);
|
|
217
|
+
expect(turnEnd?.length).toBe(1);
|
|
218
|
+
|
|
219
|
+
completeSimpleMock.mockResolvedValue({
|
|
220
|
+
content: [{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: JSON.stringify({
|
|
223
|
+
verdict: "not_done",
|
|
224
|
+
summary: "Closeout is incomplete",
|
|
225
|
+
reason: "Verification is missing",
|
|
226
|
+
actions: "run focused check",
|
|
227
|
+
checklist: [],
|
|
228
|
+
notify: true,
|
|
229
|
+
}),
|
|
230
|
+
}],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await handlers.session_start?.[0]?.({}, ctx);
|
|
234
|
+
await preflight;
|
|
235
|
+
await turnEnd;
|
|
239
|
+
|
|
240
|
+
const state = readAdvisorState();
|
|
241
|
+
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
242
|
+
expect(state.followUp).toBe("Closeout is incomplete — run focused check");
|
|
243
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
244
|
+
expect.objectContaining({
|
|
245
|
+
customType: "advisor:llm",
|
|
246
|
+
content: expect.stringContaining("Actions: run focused check"),
|
|
247
|
+
details: expect.objectContaining({ actions: ["run focused check"] }),
|
|
248
|
+
}),
|
|
249
|
+
expect.anything(),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("renders manual advisor answers as advisor custom messages", async () => {
|
|
254
|
+
expect(commands.advisor).toBeTruthy();
|
|
255
|
+
|
|
256
|
+
completeSimpleMock.mockResolvedValue({
|
|
257
|
+
content: [{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: "Post-turn review: no merge blockers identified from the session brief.",
|
|
260
|
+
}],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await commands.advisor.handler("should we merge this pr?", ctx);
|
|
264
|
+
|
|
265
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
266
|
+
expect.objectContaining({
|
|
267
|
+
customType: "advisor:llm",
|
|
268
|
+
content: "Post-turn review: no merge blockers identified from the session brief.",
|
|
269
|
+
display: true,
|
|
270
|
+
details: expect.objectContaining({
|
|
271
|
+
kind: "answer",
|
|
272
|
+
summary: "Post-turn review: no merge blockers identified from the session brief.",
|
|
273
|
+
}),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
186
278
|
it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
|
|
187
279
|
const preflight = handlers.before_agent_start;
|
|
188
280
|
const agentEnd = handlers.agent_end;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-bundle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Public Pi-Rogue bundle for advisor and orchestration. Single consolidated artefact (advisor and orchestration releases paused; their packages are private and bundled here).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|